В данном задании нужно будет:
Описание данных:
Обращаем внимание, что не все люди используют только один браузер, поэтому в столбце userID есть повторяющиеся идентификаторы. В предлагаемых данных уникальным является сочетание userID и browser.
In [208]:
from __future__ import division
import numpy as np
import pandas as pd
from scipy import stats
from statsmodels.sandbox.stats.multicomp import multipletests
%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
In [2]:
ab_data = pd.read_csv('ab_browser_test.csv')
ab_data.info()
In [3]:
ab_data.head()
Out[3]:
In [4]:
#transform 'browser' column to int
ab_data.browser = [int(ab_data.browser[i][9:]) for i in range(ab_data.shape[0])]
In [5]:
ab_data.head(10)
Out[5]:
Основная метрика, на которой мы сосредоточимся в этой работе, — это количество пользовательских кликов на web-странице в зависимости от тестируемого изменения этой страницы.
Посчитаем, насколько в группе exp больше пользовательских кликов по сравнению с группой control в процентах от числа кликов в контрольной группе.
In [6]:
#number of people in exp and control groups
ab_data.slot.value_counts()
Out[6]:
Примем в первом приближении, что количество человек в каждой из групп одинаково.
In [7]:
#indices split by groups
exp = ab_data.slot.loc[ab_data.slot == 'exp'].index
ctrl = ab_data.slot.loc[ab_data.slot == 'control'].index
In [8]:
#assumption error
err = 1 - ab_data.slot.loc[exp].shape[0] / ab_data.slot.loc[ctrl].shape[0]
print('Assumption error: %.4f' % err)
In [9]:
#total number of clicks in each group
exp_cl_num = ab_data.n_clicks.loc[exp].sum()
ctrl_cl_num = ab_data.n_clicks.loc[ctrl].sum()
print('Total number of clicks in each group')
print('Exp: %d' % exp_cl_num)
print('Control: %d' % ctrl_cl_num)
In [10]:
#proportion increase of clicks for exp over control
prop_inc_clicks = (exp_cl_num / ctrl_cl_num - 1) * 100
print('Proportion increase of clicks for exp over control: %.3f%%' % prop_inc_clicks)
Давайте попробуем посмотреть более внимательно на разницу между двумя группами (control и exp) относительно количества пользовательских кликов.
Для этого построим с помощью бутстрепа 95% доверительный интервал для средних значений и медиан количества кликов в каждой из двух групп.
In [11]:
#Clicks mean values
exp_cl_mean = ab_data.n_clicks.loc[exp].mean()
ctrl_cl_mean = ab_data.n_clicks.loc[ctrl].mean()
print('Mean number of clicks in each group')
print('Exp: %.4f' % exp_cl_mean)
print('Control: %.4f' % ctrl_cl_mean)
print('')
#Clicks median values
exp_cl_mean = ab_data.n_clicks.loc[exp].median()
ctrl_cl_mean = ab_data.n_clicks.loc[ctrl].median()
print('Median number of clicks in each group')
print('Exp: %d' % exp_cl_mean)
print('Control: %d' % ctrl_cl_mean)
In [15]:
def get_bootstrap_samples(data, n_samples):
indices = np.random.randint(0, len(data), (n_samples, len(data)))
samples = data[indices]
return samples
In [13]:
def stat_intervals(stat, alpha):
boundaries = np.percentile(stat, [100 * alpha / 2., 100 * (1 - alpha / 2.)])
return boundaries
In [108]:
%%time
#confidence intervals estimation
np.random.seed(0)
num_of_samples = 500
exp_cl_mean, ctrl_cl_mean = np.empty(num_of_samples), np.empty(num_of_samples)
exp_cl_median, ctrl_cl_median = np.empty(num_of_samples), np.empty(num_of_samples)
ctrl_cl_var = np.empty(num_of_samples)
exp_data = get_bootstrap_samples(ab_data.n_clicks.loc[exp].values, num_of_samples)
ctrl_data = get_bootstrap_samples(ab_data.n_clicks.loc[ctrl].values, num_of_samples)
for i in range(num_of_samples):
exp_cl_mean[i], ctrl_cl_mean[i] = exp_data[i].mean(), ctrl_data[i].mean()
exp_cl_median[i], ctrl_cl_median[i] = np.median(exp_data[i]), np.median(ctrl_data[i])
ctrl_cl_var[i] = ctrl_data[i].var()
In [109]:
delta_mean = map(lambda x: x[0] - x[1], zip(exp_cl_mean, ctrl_cl_mean))
delta_median = map(lambda x: x[0] - x[1], zip(exp_cl_median, ctrl_cl_median))
delta_mean_bnd = stat_intervals(delta_mean, 0.05)
delta_median_bnd = stat_intervals(delta_median, 0.05)
print('Conf. int. delta mean: [%.4f, %.4f]' % (delta_mean_bnd[0], delta_mean_bnd[1]))
print('Conf. int. delta median: [%d, %d]' % (delta_median_bnd[0], delta_median_bnd[1]))
print('legend: diff = exp - control')
Поскольку данных достаточно много (порядка полумиллиона уникальных пользователей), отличие в несколько процентов может быть не только практически значимым, но и значимым статистически. Последнее утверждение нуждается в дополнительной проверке.
In [110]:
_ = plt.figure(figsize=(15,5))
_ = plt.subplot(121)
_ = plt.hist(ab_data.n_clicks.loc[exp], bins=100)
_ = plt.title('Experiment group')
_ = plt.subplot(122)
_ = plt.hist(ab_data.n_clicks.loc[ctrl], bins=100)
_ = plt.title('Control group')
t-критерий Стьюдента имеет множество достоинств, и потому его достаточно часто применяют в AB экспериментах. Иногда его применение может быть необоснованно из-за сильной скошенности распределения данных.
Для простоты рассмотрим одновыборочный t-критерий. Чтобы действительно предположения t-критерия выполнялись необходимо, чтобы:
Оба этих предположения можно проверить с помощью бутстрепа. Ограничимся сейчас только контрольной группой, в которой распределение кликов будем называть данными в рамках данного вопроса.
Поскольку мы не знаем истинного распределения генеральной совокупности, мы можем применить бутстреп, чтобы понять, как распределены среднее значение и выборочная дисперсия.
Для этого
In [123]:
#probability plot for means
_ = stats.probplot(ctrl_cl_mean, plot=plt, rvalue=True)
_ = plt.title('Probability plot for means')
In [125]:
#probability plot for variances
_ = stats.probplot(ctrl_cl_var, plot=plt, dist='chi2', sparams=(ctrl_cl_mean.shape[0]-1), rvalue=True)
_ = plt.title('Probability plot for variances')
Одним из возможных аналогов t-критерия, которым можно воспрользоваться, является тест Манна-Уитни. На достаточно обширном классе распределений он является асимптотически более эффективным, чем t-критерий, и при этом не требует параметрических предположений о характере распределения.
Разделим выборку на две части, соответствующие control и exp группам. Преобразуем данные к виду, чтобы каждому пользователю соответствовало суммарное значение его кликов. С помощью критерия Манна-Уитни проверим гипотезу о равенстве средних.
In [179]:
users_nclicks_exp = ab_data.loc[exp].groupby(['userID', 'browser']).sum().loc[:,'n_clicks']
users_nclicks_ctrl = ab_data.loc[ctrl].groupby(['userID', 'browser']).sum().loc[:,'n_clicks']
users_nclicks_exp.head()
users_nclicks_ctrl.head()
Out[179]:
Out[179]:
In [206]:
stats.mannwhitneyu(users_nclicks_exp, users_nclicks_ctrl, alternative='two-sided')
Out[206]:
Проверим, для какого из браузеров наиболее сильно выражено отличие между количеством кликов в контрольной и экспериментальной группах.
Для этого применим для каждого из срезов (по каждому из уникальных значений столбца browser) критерий Манна-Уитни между control и exp группами и сделаем поправку Холма-Бонферрони на множественную проверку с α=0.05.
In [183]:
browsers_nclicks_exp = ab_data.loc[exp].groupby(['browser', 'userID']).sum().loc[:,'n_clicks']
browsers_nclicks_ctrl = ab_data.loc[ctrl].groupby(['browser', 'userID']).sum().loc[:,'n_clicks']
browsers_nclicks_exp.head()
browsers_nclicks_ctrl.head()
Out[183]:
Out[183]:
In [210]:
#Unique browsers
browsers = np.unique(ab_data.browser)
print('Unique browsers numbers: ' + str(browsers))
print('')
print('Mann-Whitney rank test without multipletest')
mw_p = np.empty(browsers.shape[0])
for i, br in enumerate(browsers):
print('Browser #%d: ' % br),
_, mw_p[i] = stats.mannwhitneyu(browsers_nclicks_exp.loc[br, :], browsers_nclicks_ctrl.loc[br, :], alternative='two-sided')
print('p-value = %.4f' % mw_p[i])
print('')
print('Mann-Whitney rank test with multipletest')
_, mw_p_corr, _, _ = multipletests(mw_p, alpha = 0.05, method = 'holm')
for i, br in enumerate(browsers):
print('Browser #%d: ' % br),
print('p-value = %.4f' % mw_p_corr[i])
Для каждого браузера в каждой из двух групп (control и exp) посчитаем долю запросов, в которых пользователь не кликнул ни разу. Это можно сделать, поделив сумму значений n_nonclk_queries на сумму значений n_queries. Умножив это значение на 100, получим процент некликнутых запросов, который можно легче проинтерпретировать.
In [239]:
browsers_nonclk_q_exp = ab_data.loc[exp].groupby(['browser']).sum().loc[:,'n_nonclk_queries']
browsers_clk_q_exp = ab_data.loc[exp].groupby(['browser']).sum().loc[:,'n_queries']
browsers_nonclk_q_prop_exp = browsers_nonclk_q_exp / browsers_clk_q_exp
browsers_nonclk_q_ctrl = ab_data.loc[ctrl].groupby(['browser']).sum().loc[:,'n_nonclk_queries']
browsers_clk_q_ctrl = ab_data.loc[ctrl].groupby(['browser']).sum().loc[:,'n_queries']
browsers_nonclk_q_prop_ctrl = browsers_nonclk_q_ctrl / browsers_clk_q_ctrl
print('Control / experimental groups')
for br in browsers:
print('Browser #%d' % br),
print(browsers_nonclk_q_prop_ctrl.loc[browsers_nonclk_q_prop_ctrl.index == br].values),
print('/'),
print(browsers_nonclk_q_prop_exp.loc[browsers_nonclk_q_prop_ctrl.index == br].values)